/** * [[include:modules/toolbar/button/README.md]] * @packageDocumentation * @module modules/toolbar/button */ import './button.less'; import type { Controls, IControlType, IControlTypeStrong, IControlTypeStrongList, IPopup, IToolbarButton, IToolbarCollection, IViewBased, Nullable } from 'jodit/types'; import { UIButton, UIButtonState } from 'jodit/core/ui/button'; import { autobind, component, watch } from 'jodit/core/decorators'; import { Dom } from 'jodit/core/dom'; import { Popup } from 'jodit/core/ui/popup/popup'; import { makeCollection } from 'jodit/modules/toolbar/factory'; import { attr, call, camelCase, isArray, isFunction, isJoditObject, isString, keys, position } from 'jodit/core/helpers'; import { Icon } from 'jodit/core/ui/icon'; import { ToolbarCollection } from 'jodit/modules/toolbar/collection/collection'; import { STATUSES } from 'jodit/core/component/statuses'; import { findControlType } from 'jodit/core/ui/helpers/get-control-type'; @component export class ToolbarButton extends UIButton implements IToolbarButton { override readonly state = { ...UIButtonState(), theme: 'toolbar', currentValue: '', hasTrigger: false }; protected trigger!: HTMLElement; private openedPopup: Nullable = null; constructor( jodit: T, readonly control: IControlTypeStrong, readonly target: Nullable = null ) { super(jodit); // console.log('this.button: ', this.button); // console.log('this.trigger: ', this.trigger); // Prevent lost focus jodit.e.on([this.button, this.trigger], 'mousedown', (e: MouseEvent) => e.preventDefault() ); this.onAction(this.onClick); this.hookStatus(STATUSES.ready, () => { this.initFromControl(); this.initTooltip(); this.update(); }); if (control.mods) { Object.keys(control.mods).forEach(mod => { control.mods && this.setMod(mod, control.mods[mod]); }); } } /** * Button element */ get button(): HTMLElement { return this.container.querySelector( `button.${this.componentName}__button` ) as HTMLElement; } /** * Get parent toolbar */ protected get toolbar(): Nullable { return this.closest(ToolbarCollection); } /** @override */ override className(): string { return 'ToolbarButton'; } /** @override **/ override update(): void { const { control, state } = this, tc = this.closest(ToolbarCollection) as ToolbarCollection; state.disabled = this.calculateDisabledStatus(tc); state.activated = this.calculateActivatedStatus(tc); if (isFunction(control.update) && tc) { control.update(this, tc.jodit); } super.update(); } /** @override */ override focus(): void { this.container.querySelector('button')?.focus(); } override destruct(): any { this.closePopup(); return super.destruct(); } /** @override */ protected override onChangeActivated(): void { attr(this.button, 'aria-pressed', this.state.activated); super.onChangeActivated(); } /** @override */ protected override onChangeText(): void { if (isFunction(this.control.template)) { this.text.innerHTML = this.control.template( this.j, this.control.name, this.j.i18n(this.state.text) ); } else { super.onChangeText(); } this.setMod('text-icons', Boolean(this.text.innerText.trim().length)); } /** @override */ protected override onChangeTabIndex(): void { attr(this.button, 'tabindex', this.state.tabIndex); } @watch('state.tooltip') protected override onChangeTooltip(): void { attr(this.button, 'aria-label', this.state.tooltip); super.onChangeTooltip(); } /** @override */ protected override createContainer(): HTMLElement { const cn = this.componentName; const container = this.j.c.span(cn); const button = super.createContainer(); attr(container, 'role', 'listitem'); button.classList.remove(cn); button.classList.add(cn + '__button'); Object.defineProperty(button, 'component', { value: this }); container.appendChild(button); this.trigger = this.j.c.fromHTML( `${Icon.get( 'chevron' )}` ); // console.log('container: ', container); return container; } @watch('state.hasTrigger') protected onChangeHasTrigger(): void { if (this.state.hasTrigger) { this.container.appendChild(this.trigger); } else { Dom.safeRemove(this.trigger); } this.setMod('with-trigger', this.state.hasTrigger || null); } /** @override */ protected override onChangeDisabled(): void { const dsb = this.state.disabled ? 'disabled' : null; attr(this.trigger, 'disabled', dsb); attr(this.button, 'disabled', dsb); attr(this.container, 'disabled', dsb); } /** * Add tooltip to button */ protected initTooltip(): void { if ( !this.j.o.textIcons && this.j.o.showTooltip && !this.j.o.useNativeTooltip ) { this.j.e .off(this.container, 'mouseenter mouseleave') .on(this.container, 'mousemove', (e: MouseEvent) => { if (!this.state.tooltip) { return; } !this.state.disabled && this.j.e.fire( 'delayShowTooltip', () => ({ x: e.clientX + 10, y: e.clientY + 10 }), this.state.tooltip ); }) .on(this.container, 'mouseleave', () => { this.j.e.fire('hideTooltip'); }); } } /** * Click on trigger button */ @watch('trigger:click') protected onTriggerClick(e: MouseEvent): void { if (this.openedPopup) { this.closePopup(); return; } const { control: ctr } = this; e.buffer = { actionTrigger: this }; if (ctr.list) { return this.openControlList(ctr as IControlTypeStrongList); } if (isFunction(ctr.popup)) { const popup = this.openPopup(); popup.parentElement = this; if ( this.j.e.fire( camelCase(`before-${ctr.name}-open-popup`), this.target, ctr, popup ) !== false ) { const target = this.toolbar?.getTarget(this) ?? this.target ?? null; const elm = ctr.popup( this.j, target, ctr, this.closePopup, this ); let titleElm; if (ctr.popupTitle) { titleElm = ctr.popupTitle( this.j, target, ctr, this.closePopup, this ); } if (elm) { popup .setContent( isString(elm) ? this.j.c.fromHTML(elm) : elm, titleElm ? isString(titleElm) ? this.j.c.fromHTML(titleElm) : titleElm : '', ctr?.popupContentExtraClassName ) .open( () => position(this.container), false, this.j.o.allowTabNavigation ? this.container : undefined ); } } /** * Fired after popup was opened for some control button */ /** * Close all opened popups */ this.j.e.fire( camelCase(`after-${ctr.name}-open-popup`), popup.container ); } } @autobind protected onOutsideClick(e: MouseEvent): void { if (!this.openedPopup) { return; } if ( !e || !Dom.isNode(e.target) || (!Dom.isOrContains(this.container, e.target) && !this.openedPopup.isOwnClick(e)) ) { this.closePopup(); } } /** * Click handler */ protected onClick(originalEvent: MouseEvent): void { const { control: ctr } = this; if (isFunction(ctr.exec)) { const target = this.toolbar?.getTarget(this) ?? this.target ?? null; const result = ctr.exec(this.j, target, { control: ctr, originalEvent, button: this }); // For memorise exec if (result !== false && result !== true) { this.j?.e?.fire('synchro'); if (this.parentElement) { this.parentElement.update(); } /** * Fired after calling `button.exec` function */ this.j?.e?.fire('closeAllPopups afterExec'); } if (result !== false) { return; } } if (ctr.list) { return this.openControlList(ctr as IControlTypeStrongList); } if (isFunction(ctr.popup)) { return this.onTriggerClick(originalEvent); } if (ctr.command || ctr.name) { call( isJoditObject(this.j) ? this.j.execCommand.bind(this.j) : this.j.od.execCommand.bind(this.j.od), ctr.command || ctr.name, false, ctr.args && ctr.args[0] ); this.j.e.fire('closeAllPopups'); } } /** * Calculates whether the button is active */ private calculateActivatedStatus(tc?: ToolbarCollection): boolean { if (isJoditObject(this.j) && !this.j.editorIsActive) { return false; } if ( isFunction(this.control.isActive) && this.control.isActive(this.j, this.control, this) ) { return true; } return Boolean(tc && tc.shouldBeActive(this)); } /** * Calculates whether an element is blocked for the user */ private calculateDisabledStatus(tc?: ToolbarCollection): boolean { if (this.j.o.disabled) { return true; } if ( this.j.o.readonly && (!this.j.o.activeButtonsInReadOnly || !this.j.o.activeButtonsInReadOnly.includes(this.control.name)) ) { return true; } if ( isFunction(this.control.isDisabled) && this.control.isDisabled(this.j, this.control, this) ) { return true; } return Boolean(tc && tc.shouldBeDisabled(this)); } /** * Init constant data from control */ private initFromControl(): void { const { control: ctr, state } = this; this.updateSize(); state.name = ctr.name; const { textIcons } = this.j.o; // console.log('state.name: ', state.name); // console.log('textIcons: ', textIcons); if ( textIcons === true || (isFunction(textIcons) && textIcons(ctr.name)) || ctr.template ) { state.icon = UIButtonState().icon; state.text = ctr.text || ctr.name; } else { if (ctr.iconURL) { state.icon.iconURL = ctr.iconURL; } else { const name = ctr.icon || ctr.name; state.icon.name = Icon.exists(name) || this.j.o.extraIcons?.[name] ? name : ''; } if (!ctr.iconURL && !state.icon.name) { state.text = ctr.text || ctr.name; } } if (ctr.tooltip) { state.tooltip = this.j.i18n( isFunction(ctr.tooltip) ? ctr.tooltip(this.j, ctr, this) : ctr.tooltip ); } state.hasTrigger = Boolean( !ctr.ignoreTrigger && (ctr.list || (ctr.popup && ctr.exec)) ); } /** * Create and open popup list */ private openControlList(control: IControlTypeStrongList): void { const controls: Controls = this.jodit.options.controls ?? {}, getControl = (key: string): IControlType | void => findControlType(key, controls); const list = control.list, menu = this.openPopup(), toolbar = makeCollection(this.j); if (control.name) { toolbar.container.classList.add( `${toolbar.componentName}__${control.name}` ); } menu.parentElement = this; toolbar.parentElement = menu; toolbar.mode = 'vertical'; const getButton = ( key: string, value: string | number | object ): IControlTypeStrong => { if (isString(value) && getControl(value)) { return { name: value.toString(), ...getControl(value) }; } if (isString(key) && getControl(key)) { return { name: key.toString(), ...getControl(key), ...(typeof value === 'object' ? value : {}) }; } const { childTemplate } = control; const childControl: IControlTypeStrong = { name: key.toString(), template: childTemplate && ((j, k, v): string => childTemplate(j, k, v, this)), exec: control.exec, data: control.data, command: control.command, isActive: control.isChildActive, isDisabled: control.isChildDisabled, mode: control.mode, args: [...(control.args ? control.args : []), key, value] }; if (isString(value)) { childControl.text = value; } return childControl; }; toolbar.build( isArray(list) ? list.map(getButton) : keys(list, false).map(key => getButton(key, list[key])), this.target ); menu.setContent(toolbar.container).open( () => position(this.container), false, this.j.o.allowTabNavigation ? this.container : undefined ); this.state.activated = true; } private openPopup(): IPopup { this.closePopup(); this.openedPopup = new Popup(this.j, false); this.j.e .on(this.ow, 'mousedown touchstart', this.onOutsideClick) .on('escape closeAllPopups', this.onOutsideClick); return this.openedPopup; } @autobind private closePopup(): void { if (this.openedPopup) { this.j.e .off(this.ow, 'mousedown touchstart', this.onOutsideClick) .off('escape closeAllPopups', this.onOutsideClick); this.state.activated = false; this.openedPopup.close(); this.openedPopup.destruct(); this.openedPopup = null; } } }